Skip to content

Playwright E2E テストスイートを追加#4

Merged
dotani1111 merged 2 commits intomainfrom
feature/e2e-tests
Mar 24, 2026
Merged

Playwright E2E テストスイートを追加#4
dotani1111 merged 2 commits intomainfrom
feature/e2e-tests

Conversation

@dotani1111
Copy link
Copy Markdown
Owner

@dotani1111 dotani1111 commented Mar 22, 2026

Summary

  • 管理画面の主要機能をカバーする25件のPlaywright E2Eテストを追加
  • GitHub Actions CIにE2Eジョブを追加(MySQL + MailHog環境で自動実行)
  • テストメールのダミー商品にIDを設定し、メール本文の商品名表示を修正

テストカバレッジ

カテゴリ テスト数
ナビゲーション 3
設定画面 6
テストメール送信 3
メールテンプレート 2
コマンド実行 4
送信履歴 3
異常系・エッジケース 3
セットアップ 1

Test plan

  • ローカルで全25テストがパス
  • CI E2Eジョブが正常に完了すること
  • 既存のlint/PHPUnitジョブに影響がないこと

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • 在庫アラートの包括的なE2Eテストを追加(設定画面、送信履歴、テストメール、エラー・エッジケース含む)
    • MailHog連携で送信されたメールの件名・本文・宛先を自動検証
    • Playwrightのグローバルセットアップと各種ヘルパーを導入し、テスト操作を自動化
  • Chores

    • E2E用のパッケージ設定と.gitignoreを追加
    • GitHub ActionsにE2Eワークフローを追加、既存CIを調整(Lintワークフロー新設を含む)

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Playwrightベースの包括的なE2Eテスト群とCIワークフロー、MailHog/ページ/コマンドのテストヘルパーを追加し、テスト用ダミーProductに決定論的IDを割り当てるServiceの小修正を含みます。

Changes

Cohort / File(s) Summary
E2E 設定 & メタ
e2e/.gitignore, e2e/package.json, e2e/playwright.config.ts
Playwrightプロジェクト初期設定、依存・スクリプト、テスト実行設定を追加しています。CIでの実行前提が整っています。
E2E ヘルパー
e2e/helpers/command.ts, e2e/helpers/mailhog.ts, e2e/helpers/pages.ts
CLI実行ラッパー、MailHog API操作+RFC2047/quoted-printableデコード、管理画面操作ユーティリティを追加。デコード周りやタイムアウト/ポーリングの挙動は注意が必要かもしれません。
E2E テスト群
e2e/tests/.../*.spec.ts, e2e/tests/global-setup.ts
ナビゲーション、設定、テストメール、コマンド実行、送信履歴、メールテンプレート、エッジケース等のPlaywrightテストを多数追加。auth.jsonを用いたストレージStateとglobal-setupを導入しています。
CIワークフロー
.github/workflows/e2e.yml, .github/workflows/lint.yml, .github/workflows/test.yml
E2Eジョブ(MySQL・MailHogサービス含む)を追加、Lintワークフローを導入し既存CIのトリガ/ジョブを調整しています。ワークフロー内のEC-CUBEチェックアウトやプラグイン配置手順に注意が要るかもしれません。
Service 修正
Service/StockAlertMailBuilder.php
createDummyItemsでダミーProductに決定論的なIDをReflectionで設定するprivateヘルパーを追加しています。非公開プロパティに直接触れる点は…少し気になるところがあります。

Sequence Diagram(s)

sequenceDiagram
    participant Test as Playwright テスト
    participant Web as EC-CUBE (PHP)
    participant CLI as bin/console (PHP CLI)
    participant DB as MySQL
    participant Mailhog as MailHog API

    Test->>Web: 管理画面で設定変更 / テンプレート編集 / 送信テスト実行
    Web->>DB: 設定保存・ログ更新
    Test->>CLI: execConsole("eccube:plugin:stock-alert-mail")
    CLI->>DB: 商品抽出・ログ登録
    CLI->>Mailhog: SMTP送信(MailHog受信)
    Mailhog-->>Test: HTTP APIでメッセージ取得(waitForMessage)
    Test->>Web: 送信履歴や画面表示を検証
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

在庫の夜をそっと数えて
MailHogは静かに手紙を抱く
Playwrightは窓辺で見張るように
ダミーに番号を与え、道筋つけて
ささやかなテストの芽が育ちますね 🌙

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルはPlaywright E2Eテストスイート追加という実装内容を明確に反映しており、変更内容全体の主要な要素を適切にまとめています。
Docstring Coverage ✅ Passed Docstring coverage is 95.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/e2e-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (16)
e2e/helpers/pages.ts (2)

35-36: ダイアログリスナーの登録をonceに変更することを推奨します。

page.on('dialog', ...)はリスナーを永続的に登録します。同一ページコンテキストでsendTestMailを複数回呼び出すとリスナーが蓄積する可能性があります。page.once('dialog', ...)を使用することで、一度だけ処理されるようになります。

♻️ 提案される修正
   // confirm ダイアログを自動承認
-  page.on('dialog', dialog => dialog.accept());
+  page.once('dialog', dialog => dialog.accept());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/pages.ts` around lines 35 - 36, Replace the persistent dialog
listener registration page.on('dialog', dialog => dialog.accept()) with a
one-time listener so the handler is removed after it fires (use
page.once('dialog', ...) instead); update the place where sendTestMail triggers
dialogs to register the one-shot listener before the action to prevent listener
accumulation across multiple sendTestMail calls and ensure each dialog is
handled exactly once.

38-38: セレクタをより具体的にすることを検討してください。

button.btn-outline-primaryは汎用的なセレクタです。UIの変更でテストが不安定になる可能性があります。可能であれば、data-testid属性の追加やより具体的なセレクタの使用を検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/pages.ts` at line 38, The test uses a fragile generic selector
page.locator('button.btn-outline-primary').click() — replace it with a stable
data-testid-based selector: add a specific data-testid attribute to the target
component (e.g., data-testid="confirm-button") and update the test to locate
that element (use page.getByTestId or page.locator('[data-testid="..."]') and
call click()); ensure the test and the UI component that renders the button (the
component/class that currently renders button.btn-outline-primary) are both
updated so the test targets the new data-testid.
e2e/helpers/mailhog.ts (1)

23-32: fetch呼び出しにエラーハンドリングの追加を検討してください。

deleteAllMessagesgetMessagesはHTTPエラー(4xx/5xx)やネットワークエラーをチェックしていません。MailHogが利用不可の場合、エラーメッセージが不明確になる可能性があります。

♻️ 提案される修正
 /** 全メールを削除する */
 export async function deleteAllMessages(): Promise<void> {
-  await fetch(`${MAILHOG_API}/v1/messages`, { method: 'DELETE' });
+  const res = await fetch(`${MAILHOG_API}/v1/messages`, { method: 'DELETE' });
+  if (!res.ok) {
+    throw new Error(`MailHog API error: ${res.status} ${res.statusText}`);
+  }
 }
 
 /** 全メールを取得する(新しい順) */
 export async function getMessages(): Promise<MailhogMessage[]> {
   const res = await fetch(`${MAILHOG_API}/v2/messages`);
+  if (!res.ok) {
+    throw new Error(`MailHog API error: ${res.status} ${res.statusText}`);
+  }
   const data: MailhogResponse = await res.json();
   return data.items;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/mailhog.ts` around lines 23 - 32, The two helper functions
deleteAllMessages and getMessages lack error handling for network and HTTP
errors; update both to wrap their fetch calls in try/catch, check response.ok
after each fetch, and throw or reject with a descriptive error that includes the
HTTP status and response text (or underlying network error) so callers get clear
diagnostics when MailHog is unavailable or returns 4xx/5xx responses. Ensure
getMessages still parses JSON only after verifying response.ok and that
deleteAllMessages reports failure when the DELETE does not succeed.
e2e/tests/global-setup.ts (1)

5-6: 認証情報を環境変数から読み込むことを検討してください。

ハードコードされた認証情報(admin/password)はローカル・CI環境では一般的ですが、環境変数から読み込むようにすると柔軟性が向上します。

♻️ 提案される修正
+const ADMIN_ID = process.env.ECCUBE_ADMIN_ID || 'admin';
+const ADMIN_PASS = process.env.ECCUBE_ADMIN_PASS || 'password';
+
 setup('管理画面にログインしてセッションを保存', async ({ page }) => {
   await page.goto('/admin/login');
-  await page.locator('#login_id').fill('admin');
-  await page.locator('#password').fill('password');
+  await page.locator('#login_id').fill(ADMIN_ID);
+  await page.locator('#password').fill(ADMIN_PASS);
   await page.locator('button[type="submit"]').click();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/global-setup.ts` around lines 5 - 6, Replace the hardcoded
credentials used with page.locator('#login_id').fill('admin') and
page.locator('#password').fill('password') so they read from environment
variables (e.g., process.env.E2E_LOGIN and process.env.E2E_PASSWORD); update the
global setup logic in e2e/tests/global-setup.ts to pull those vars, provide
sensible fallback or throw a clear error when missing, and use the retrieved
values when filling the '#login_id' and '#password' locators.
e2e/tests/log.spec.ts (1)

42-58: テストの前提条件について確認してください。

TC-6.3はexecStockAlert()を実行しますが、TC-6.2も同様に実行します。テストの実行順序によっては、ログの状態が期待と異なる可能性があります。テスト間の独立性を高めるため、各テストで明示的にデータをセットアップすることを検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/log.spec.ts` around lines 42 - 58, The test TC-6.3 depends on log
state possibly set by TC-6.2 (execStockAlert()), so make the test self-contained
by explicitly creating the expected stock-alert log and ensuring the page state
before asserting the product link; call or implement a deterministic setup
helper (e.g., ensureStockAlert or awaitable execStockAlert) and a teardown/reset
helper (e.g., clearLogs) so you can call clearLogs(); await execStockAlert() (or
call the new ensureStockAlert API) and wait for the log to appear via
gotoLog(page) before querying productLink (the locator 'table
a[href*="admin/product/product"]'). If the link is truly absent then fail the
test rather than silently skip; update TC-6.3 to use setThreshold(page, 9999),
await execStockAlert(), await gotoLog(page), then assert productLink.isVisible()
and click it to expect the /admin/product/product/:id/edit URL.
e2e/tests/config.spec.ts (2)

44-45: .catch(() => false) は不要です。

isVisible() は例外をスローしないため、.catch() は冗長です。

♻️ 修正案
-    const hasSuccess = await page.locator('.alert-success').isVisible().catch(() => false);
+    const hasSuccess = await page.locator('.alert-success').isVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/config.spec.ts` around lines 44 - 45, Remove the unnecessary
.catch(() => false) from the isVisible call: change the assignment to use await
page.locator('.alert-success').isVisible() directly (the hasSuccess variable and
the expect(hasSuccess).toBeFalsy() assertion remain unchanged), since
isVisible() does not throw and the .catch is redundant.

65-66: 親要素へのロケーター参照方法をより明示的にしてください。

locator('..') は Playwright で暗黙的に XPath の親軸セレクターとして機能しますが、意図をより明確にするために locator('xpath=..') と明示的に指定することをお勧めします。これにより、他の開発者がコードを読む際に XPath セレクターを使用していることがすぐに理解できるようになります。

♻️ より明確なロケーターの提案
-    const helpText = page.locator('#stock_alert_config_alertEmails')
-      .locator('..').locator('.form-text');
+    const helpText = page.locator('#stock_alert_config_alertEmails')
+      .locator('xpath=..').locator('.form-text');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/config.spec.ts` around lines 65 - 66, Change the implicit parent
locator usage to an explicit XPath expression for clarity: update the helpText
locator that chains
page.locator('#stock_alert_config_alertEmails').locator('..').locator('.form-text')
so the parent reference uses locator('xpath=..') (i.e., locate the parent via
XPath) before chaining .locator('.form-text'); keep the same variable name
helpText and chaining behavior but replace the implicit '..' with 'xpath=..'.
e2e/tests/error-cases.spec.ts (2)

13-14: .catch(() => false) は不要です。

Playwright の isVisible() は要素が存在しない場合でも例外をスローせず false を返します。.catch() は冗長であり、削除できます。

♻️ 修正案
-    const hasSuccess = await page.locator('.alert-success').isVisible().catch(() => false);
-    const hasError = await page.locator('.alert-danger').isVisible().catch(() => false);
+    const hasSuccess = await page.locator('.alert-success').isVisible();
+    const hasError = await page.locator('.alert-danger').isVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/error-cases.spec.ts` around lines 13 - 14, Remove the redundant
.catch(() => false) wrappers around the Playwright visibility checks: replace
the uses of page.locator('.alert-success').isVisible().catch(() => false) and
page.locator('.alert-danger').isVisible().catch(() => false) with simple awaits
of page.locator(...).isVisible() (i.e., assign hasSuccess = await
page.locator('.alert-success').isVisible() and hasError = await
page.locator('.alert-danger').isVisible()), since isVisible() already returns
false for non-existent elements.

28-36: CSRF テストのアサーションが複雑で意図が不明瞭です。

現在のアサーションは、ステータスコードが [301, 302, 403, 422] のいずれか または URL に /config が含まれる場合に成功します。コンテキストスニペットによると、コントローラーは CSRF 失敗時に 302 リダイレクトを返すため、よりシンプルなアサーションで十分です。

♻️ より明確なアサーションの提案
   test('CSRF トークンなしのテストメール送信は拒否される', async ({ page }) => {
     // 直接 POST(CSRF トークンなし)
-    const response = await page.request.post('/admin/plugin/stock-alert/config/send-test');
+    const response = await page.request.post('/admin/plugin/stock-alert/config/send-test', {
+      maxRedirects: 0,
+    });

     // リダイレクト or エラーになること(200 で処理が通らない)
-    expect([301, 302, 403, 422].some(code =>
-      response.status() === code || response.url().includes('/config'),
-    )).toBeTruthy();
+    // CSRF エラー時は 302 リダイレクトが返される
+    expect(response.status()).toBe(302);
   });

注: maxRedirects: 0 を使用しない場合は、リダイレクト後の URL を確認する方法も有効です:

expect(response.url()).toContain('/config');
expect(response.status()).toBe(200); // リダイレクト後のステータス
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/error-cases.spec.ts` around lines 28 - 36, The current complex
assertion in the test "CSRF トークンなしのテストメール送信は拒否される" makes intent unclear; replace
the multi-code/URL OR check with a single clear assertion on the response
produced by the POST (the response variable) — since the controller returns a
302 on CSRF failure, assert response.status() === 302 (or alternatively assert
response.url().includes('/config') if you prefer checking the redirected
location); remove the array-of-codes logic and the combined boolean expression
to make the test deterministic and readable.
e2e/tests/test-mail.spec.ts (1)

43-51: 「メールが送信されない」ことの検証パターンを改善してください。

現在の実装には以下の問題があります:

  1. waitForTimeout(2000) は固定遅延であり、環境依存で不安定になりやすい
  2. expect(true).toBe(false) は失敗時のメッセージが不明瞭
♻️ より明確な検証パターンの提案
     // 少し待ってもメールが届かないことを確認
-    await page.waitForTimeout(2000);
-    try {
-      await waitForMessage(() => true, 1000);
-      // ここに到達したらメールが届いてしまっている
-      expect(true).toBe(false);
-    } catch {
-      // メールが届かない = 期待通り
-    }
+    // 短いタイムアウトでメッセージが届かないことを確認
+    const messages = await getMessages();
+    expect(messages).toHaveLength(0);

注: getMessages()deleteAllMessages() の後に呼び出しているため、メッセージが 0 件であることを直接確認できます。ダイアログをキャンセルした直後なので、長い待機は不要です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/test-mail.spec.ts` around lines 43 - 51, Replace the brittle
fixed-wait + try/catch pattern (waitForTimeout, waitForMessage,
expect(true).toBe(false)) with a direct assertion that the mailbox is empty
after the dialog cancellation: after calling deleteAllMessages(), call
getMessages() and assert its length is 0 with a clear expectation message;
remove the unnecessary page.waitForTimeout(2000) and the try/catch around
waitForMessage() so the test deterministically checks getMessages() === 0
instead of relying on timeouts or an inverted expect.
e2e/tests/command.spec.ts (2)

47-50: setTimeout によるハードコード遅延はテストを不安定にします。

new Promise(r => setTimeout(r, 2000)) は固定遅延であり、環境によってはタイミング問題が発生する可能性があります。MailHog のメッセージが「ない」ことを確認するには、短いタイムアウトで getMessages() を呼び出し、空であることをアサートする方が信頼性が高いです。

♻️ 改善案
     // メールが送信されていないことを確認
-    await new Promise(r => setTimeout(r, 2000));
-    const messages = await getMessages();
-    expect(messages.length).toBe(0);
+    // 短いポーリング間隔でメッセージがないことを確認
+    const checkNoMessages = async (retries = 3, delay = 500) => {
+      for (let i = 0; i < retries; i++) {
+        const messages = await getMessages();
+        if (messages.length > 0) {
+          throw new Error(`Unexpected messages found: ${messages.length}`);
+        }
+        await new Promise(r => setTimeout(r, delay));
+      }
+    };
+    await checkNoMessages();

または、waitForMessage のような既存ヘルパーに「メッセージがないことを確認する」バリアントを追加することも検討してください。

Also applies to: 67-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/command.spec.ts` around lines 47 - 50, The test uses a hardcoded
delay (new Promise(... setTimeout 2000)) which makes it flaky; replace that
sleep with a direct, short-polling check that calls getMessages() (or a small
retry loop with a short overall timeout) and assert that the returned array is
empty, or add/use a helper variant like waitForMessageAbsent/getMessagesEmpty to
encapsulate this check; apply the same fix to the other occurrence around the
67-70 block so both assertions verify emptiness via getMessages() (or the new
helper) instead of sleeping.

97-103: test.afterAlltest.describe ブロックの外にあります。

test.afterAlltest.describe('コマンド実行') の外に配置されているため、このファイル内のすべてのテストに適用されます。意図通りであれば問題ありませんが、スコープを明確にするために test.describe 内に移動することを推奨します。

♻️ スコープを明確にする修正案
 test.describe('コマンド実行', () => {
   test.beforeEach(async () => {
     await deleteAllMessages();
   });

+  test.afterAll(async ({ browser }) => {
+    // 閾値をデフォルトに戻す
+    const context = await browser.newContext({ storageState: './auth.json' });
+    const page = await context.newPage();
+    await setThreshold(page, 5);
+    await context.close();
+  });
+
   test('TC-5.1: アラート対象商品ありでメール送信される', async ({ page }) => {
     // ...
   });
   // ...
 });
-
-test.afterAll(async ({ browser }) => {
-  // 閾値をデフォルトに戻す
-  const context = await browser.newContext({ storageState: './auth.json' });
-  const page = await context.newPage();
-  await setThreshold(page, 5);
-  await context.close();
-});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/command.spec.ts` around lines 97 - 103, test.afterAll が
test.describe('コマンド実行') ブロックの外にありファイル全体に適用されているため、意図がコマンド実行テストだけに適用するなら
test.describe 内に移動してください;具体的には現在の test.afterAll ブロック(browser を受け取り
setThreshold(page, 5) を呼ぶ処理)を test.describe('コマンド実行')
の末尾に移してスコープを限定し、setThreshold 呼び出しと context/newPage/close の処理がその describe
内の全テスト後にのみ実行されるようにしてください。
.github/workflows/ci.yml (2)

197-201: MailHog サービスにヘルスチェックがありません。

MySQL サービスと異なり、MailHog にはヘルスチェックが設定されていません。テストの安定性向上のため、options でヘルスチェックを追加することを検討してください。

♻️ MailHog ヘルスチェックの追加案
       mailhog:
         image: mailhog/mailhog
         ports:
           - 1025:1025
           - 8025:8025
+        options: >-
+          --health-cmd="wget -q --spider http://localhost:8025/api/v2/messages || exit 1"
+          --health-interval=10s
+          --health-timeout=5s
+          --health-retries=3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 197 - 201, The MailHog service block
(mailhog) lacks a container healthcheck; add an options.healthcheck entry for
the mailhog service to improve test stability by probing the web UI or SMTP port
(e.g., a command like curl -f http://localhost:8025/ || exit 1), and configure
sensible interval, timeout and retries (for example interval: 10s, timeout: 5s,
retries: 5) so CI waits for MailHog readiness before running tests.

261-264: PHP サーバーの起動確認が不十分な可能性があります。

sleep 3 と単一の curl チェックでは、サーバー起動のタイミング問題でテストが不安定になる可能性があります。リトライ付きのヘルスチェックを推奨します。

♻️ リトライ付きヘルスチェックの提案
         run: |
           php -S 127.0.0.1:8000 -t . &
-          sleep 3
-          curl -sf http://127.0.0.1:8000/admin/login > /dev/null
+          for i in {1..30}; do
+            curl -sf http://127.0.0.1:8000/admin/login > /dev/null && break
+            echo "Waiting for server... ($i)"
+            sleep 1
+          done
+          curl -sf http://127.0.0.1:8000/admin/login > /dev/null || exit 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 261 - 264, Replace the fragile "sleep
3" + single "curl -sf http://127.0.0.1:8000/admin/login" check with a retrying
health-check loop: after launching the PHP built-in server (php -S
127.0.0.1:8000 -t . &), poll the admin URL with curl in a loop (e.g., up to N
attempts with a short sleep between attempts), exit successfully when curl -sf
returns 0 and exit with non‑zero if the max attempts are reached; update the run
step that currently uses "sleep 3" and the single "curl" invocation to use this
retry loop to ensure reliable startup before proceeding.
e2e/playwright.config.ts (1)

8-14: trace: 'on-first-retry'retries: 0 では無効です。

retries: 0 が設定されているため、リトライが発生せず trace: 'on-first-retry' によるトレース収集は行われません。デバッグ用にトレースを取得したい場合は、trace: 'on' または trace: 'retain-on-failure' に変更することを検討してください。

♻️ トレース設定の修正案
   use: {
     baseURL: process.env.ECCUBE_URL || 'http://127.0.0.1:8000',
     screenshot: 'only-on-failure',
-    trace: 'on-first-retry',
+    trace: 'retain-on-failure',
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/playwright.config.ts` around lines 8 - 14, The current Playwright config
sets retries: 0 so trace: 'on-first-retry' never triggers; update the trace
setting inside the use object in e2e/playwright.config.ts (the trace property in
the use block) to a mode that captures traces without retries (e.g., change
trace to 'on' or 'retain-on-failure'), or alternatively increase retries from 0
if you want to keep 'on-first-retry' behavior; modify the trace value
accordingly to ensure traces are actually collected during test runs.
e2e/tests/mail-template.spec.ts (1)

54-59: waitForTimeout の使用はテストを不安定にする可能性があります。

Playwright のドキュメントでは waitForTimeout の使用は推奨されていません。代わりに、バリデーションエラーメッセージの表示を待つか、フォームの状態変化を検出するアサーションを使用してください。

♻️ 改善案
     // 送信ボタンをクリック(HTML5 required バリデーションで送信されない場合もある)
     await page.locator('button.btn-ec-conversion[type="submit"]').click();

     // 成功メッセージが表示されないことを確認
-    await page.waitForTimeout(1000);
-    await expect(page.locator('.alert-success')).not.toBeVisible();
+    // バリデーションエラーまたはページの状態が安定するまで待機
+    await expect(page.locator('.alert-success')).not.toBeVisible({ timeout: 3000 });

注: not.toBeVisible()timeout オプションを指定すると、指定時間内に要素が表示されないことを確認できます。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/mail-template.spec.ts` around lines 54 - 59, Replace the brittle
waitForTimeout call with a deterministic assertion: after clicking the submit
button (page.locator('button.btn-ec-conversion[type="submit"]').click()), wait
for the specific validation/error element to appear or for the success alert to
remain absent using Playwright's expect assertions instead of sleep. Concretely,
remove waitForTimeout and either assert the validation message locator becomes
visible (e.g., wait for your form validation selector) or call
expect(page.locator('.alert-success')).not.toBeVisible with an explicit timeout
option to assert the alert does not appear within the given time; ensure you
reference the same selectors used in this test (the submit button locator and
'.alert-success') when making the replacement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/tests/error-cases.spec.ts`:
- Line 6: Two tests share the same ID string "TC-7.2" which will cause duplicate
test identifiers; locate the test call using the title 'TC-7.2:
テストメール送信失敗時にエラーメッセージが表示される(SMTP障害)' and the other test that reuses "TC-7.2" and
rename one to a unique ID (e.g., change one to "TC-7.3" or "TC-7.2a"), and
update any assertions/comments or external references that expect the old ID so
the test reports have distinct identifiers.

In `@e2e/tests/navigation.spec.ts`:
- Line 4: Multiple tests in navigation.spec.ts use the same ID "TC-1.1"; update
each test title string in the three test(...) declarations (e.g., the test
starting with "TC-1.1: サイドバーに在庫アラートメニューが表示される") so each has a unique test ID
like "TC-1.1", "TC-1.2", "TC-1.3" while keeping the rest of the description
intact; ensure you change all occurrences of the duplicated "TC-1.1" test titles
in this file.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 197-201: The MailHog service block (mailhog) lacks a container
healthcheck; add an options.healthcheck entry for the mailhog service to improve
test stability by probing the web UI or SMTP port (e.g., a command like curl -f
http://localhost:8025/ || exit 1), and configure sensible interval, timeout and
retries (for example interval: 10s, timeout: 5s, retries: 5) so CI waits for
MailHog readiness before running tests.
- Around line 261-264: Replace the fragile "sleep 3" + single "curl -sf
http://127.0.0.1:8000/admin/login" check with a retrying health-check loop:
after launching the PHP built-in server (php -S 127.0.0.1:8000 -t . &), poll the
admin URL with curl in a loop (e.g., up to N attempts with a short sleep between
attempts), exit successfully when curl -sf returns 0 and exit with non‑zero if
the max attempts are reached; update the run step that currently uses "sleep 3"
and the single "curl" invocation to use this retry loop to ensure reliable
startup before proceeding.

In `@e2e/helpers/mailhog.ts`:
- Around line 23-32: The two helper functions deleteAllMessages and getMessages
lack error handling for network and HTTP errors; update both to wrap their fetch
calls in try/catch, check response.ok after each fetch, and throw or reject with
a descriptive error that includes the HTTP status and response text (or
underlying network error) so callers get clear diagnostics when MailHog is
unavailable or returns 4xx/5xx responses. Ensure getMessages still parses JSON
only after verifying response.ok and that deleteAllMessages reports failure when
the DELETE does not succeed.

In `@e2e/helpers/pages.ts`:
- Around line 35-36: Replace the persistent dialog listener registration
page.on('dialog', dialog => dialog.accept()) with a one-time listener so the
handler is removed after it fires (use page.once('dialog', ...) instead); update
the place where sendTestMail triggers dialogs to register the one-shot listener
before the action to prevent listener accumulation across multiple sendTestMail
calls and ensure each dialog is handled exactly once.
- Line 38: The test uses a fragile generic selector
page.locator('button.btn-outline-primary').click() — replace it with a stable
data-testid-based selector: add a specific data-testid attribute to the target
component (e.g., data-testid="confirm-button") and update the test to locate
that element (use page.getByTestId or page.locator('[data-testid="..."]') and
call click()); ensure the test and the UI component that renders the button (the
component/class that currently renders button.btn-outline-primary) are both
updated so the test targets the new data-testid.

In `@e2e/playwright.config.ts`:
- Around line 8-14: The current Playwright config sets retries: 0 so trace:
'on-first-retry' never triggers; update the trace setting inside the use object
in e2e/playwright.config.ts (the trace property in the use block) to a mode that
captures traces without retries (e.g., change trace to 'on' or
'retain-on-failure'), or alternatively increase retries from 0 if you want to
keep 'on-first-retry' behavior; modify the trace value accordingly to ensure
traces are actually collected during test runs.

In `@e2e/tests/command.spec.ts`:
- Around line 47-50: The test uses a hardcoded delay (new Promise(... setTimeout
2000)) which makes it flaky; replace that sleep with a direct, short-polling
check that calls getMessages() (or a small retry loop with a short overall
timeout) and assert that the returned array is empty, or add/use a helper
variant like waitForMessageAbsent/getMessagesEmpty to encapsulate this check;
apply the same fix to the other occurrence around the 67-70 block so both
assertions verify emptiness via getMessages() (or the new helper) instead of
sleeping.
- Around line 97-103: test.afterAll が test.describe('コマンド実行')
ブロックの外にありファイル全体に適用されているため、意図がコマンド実行テストだけに適用するなら test.describe
内に移動してください;具体的には現在の test.afterAll ブロック(browser を受け取り setThreshold(page, 5)
を呼ぶ処理)を test.describe('コマンド実行') の末尾に移してスコープを限定し、setThreshold 呼び出しと
context/newPage/close の処理がその describe 内の全テスト後にのみ実行されるようにしてください。

In `@e2e/tests/config.spec.ts`:
- Around line 44-45: Remove the unnecessary .catch(() => false) from the
isVisible call: change the assignment to use await
page.locator('.alert-success').isVisible() directly (the hasSuccess variable and
the expect(hasSuccess).toBeFalsy() assertion remain unchanged), since
isVisible() does not throw and the .catch is redundant.
- Around line 65-66: Change the implicit parent locator usage to an explicit
XPath expression for clarity: update the helpText locator that chains
page.locator('#stock_alert_config_alertEmails').locator('..').locator('.form-text')
so the parent reference uses locator('xpath=..') (i.e., locate the parent via
XPath) before chaining .locator('.form-text'); keep the same variable name
helpText and chaining behavior but replace the implicit '..' with 'xpath=..'.

In `@e2e/tests/error-cases.spec.ts`:
- Around line 13-14: Remove the redundant .catch(() => false) wrappers around
the Playwright visibility checks: replace the uses of
page.locator('.alert-success').isVisible().catch(() => false) and
page.locator('.alert-danger').isVisible().catch(() => false) with simple awaits
of page.locator(...).isVisible() (i.e., assign hasSuccess = await
page.locator('.alert-success').isVisible() and hasError = await
page.locator('.alert-danger').isVisible()), since isVisible() already returns
false for non-existent elements.
- Around line 28-36: The current complex assertion in the test "CSRF
トークンなしのテストメール送信は拒否される" makes intent unclear; replace the multi-code/URL OR check
with a single clear assertion on the response produced by the POST (the response
variable) — since the controller returns a 302 on CSRF failure, assert
response.status() === 302 (or alternatively assert
response.url().includes('/config') if you prefer checking the redirected
location); remove the array-of-codes logic and the combined boolean expression
to make the test deterministic and readable.

In `@e2e/tests/global-setup.ts`:
- Around line 5-6: Replace the hardcoded credentials used with
page.locator('#login_id').fill('admin') and
page.locator('#password').fill('password') so they read from environment
variables (e.g., process.env.E2E_LOGIN and process.env.E2E_PASSWORD); update the
global setup logic in e2e/tests/global-setup.ts to pull those vars, provide
sensible fallback or throw a clear error when missing, and use the retrieved
values when filling the '#login_id' and '#password' locators.

In `@e2e/tests/log.spec.ts`:
- Around line 42-58: The test TC-6.3 depends on log state possibly set by TC-6.2
(execStockAlert()), so make the test self-contained by explicitly creating the
expected stock-alert log and ensuring the page state before asserting the
product link; call or implement a deterministic setup helper (e.g.,
ensureStockAlert or awaitable execStockAlert) and a teardown/reset helper (e.g.,
clearLogs) so you can call clearLogs(); await execStockAlert() (or call the new
ensureStockAlert API) and wait for the log to appear via gotoLog(page) before
querying productLink (the locator 'table a[href*="admin/product/product"]'). If
the link is truly absent then fail the test rather than silently skip; update
TC-6.3 to use setThreshold(page, 9999), await execStockAlert(), await
gotoLog(page), then assert productLink.isVisible() and click it to expect the
/admin/product/product/:id/edit URL.

In `@e2e/tests/mail-template.spec.ts`:
- Around line 54-59: Replace the brittle waitForTimeout call with a
deterministic assertion: after clicking the submit button
(page.locator('button.btn-ec-conversion[type="submit"]').click()), wait for the
specific validation/error element to appear or for the success alert to remain
absent using Playwright's expect assertions instead of sleep. Concretely, remove
waitForTimeout and either assert the validation message locator becomes visible
(e.g., wait for your form validation selector) or call
expect(page.locator('.alert-success')).not.toBeVisible with an explicit timeout
option to assert the alert does not appear within the given time; ensure you
reference the same selectors used in this test (the submit button locator and
'.alert-success') when making the replacement.

In `@e2e/tests/test-mail.spec.ts`:
- Around line 43-51: Replace the brittle fixed-wait + try/catch pattern
(waitForTimeout, waitForMessage, expect(true).toBe(false)) with a direct
assertion that the mailbox is empty after the dialog cancellation: after calling
deleteAllMessages(), call getMessages() and assert its length is 0 with a clear
expectation message; remove the unnecessary page.waitForTimeout(2000) and the
try/catch around waitForMessage() so the test deterministically checks
getMessages() === 0 instead of relying on timeouts or an inverted expect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 592f4966-b86f-48e7-9368-3f713d9f083f

📥 Commits

Reviewing files that changed from the base of the PR and between 12e3e28 and a3f1587.

⛔ Files ignored due to path filters (1)
  • e2e/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • .github/workflows/ci.yml
  • Service/StockAlertMailBuilder.php
  • e2e/.gitignore
  • e2e/helpers/command.ts
  • e2e/helpers/mailhog.ts
  • e2e/helpers/pages.ts
  • e2e/package.json
  • e2e/playwright.config.ts
  • e2e/tests/command.spec.ts
  • e2e/tests/config.spec.ts
  • e2e/tests/error-cases.spec.ts
  • e2e/tests/global-setup.ts
  • e2e/tests/log.spec.ts
  • e2e/tests/mail-template.spec.ts
  • e2e/tests/navigation.spec.ts
  • e2e/tests/test-mail.spec.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
.github/workflows/ci.yml (2)

272-275: 固定 sleep 3 はフレークの原因になりやすいかもしれません

起動完了前に curl が走ると、環境負荷次第で不安定になりやすいので、リトライ付きの待機にしておくほうが安全だと思います。

差分案
-          php -S 127.0.0.1:8000 router.php &
-          sleep 3
-          curl -sf http://127.0.0.1:8000/admin/login > /dev/null
+          php -S 127.0.0.1:8000 router.php >/tmp/eccube-php-server.log 2>&1 &
+          for i in {1..30}; do
+            if curl -sf http://127.0.0.1:8000/admin/login > /dev/null; then
+              break
+            fi
+            sleep 1
+          done
+          curl -sf http://127.0.0.1:8000/admin/login > /dev/null || {
+            echo "EC-CUBE サーバー起動確認に失敗しました"
+            tail -n 200 /tmp/eccube-php-server.log || true
+            exit 1
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 272 - 275, Replace the fixed sleep
before the curl healthcheck with a retry loop that polls the admin login
endpoint until it becomes available or a timeout is reached: run the PHP
built-in server (router.php) in background as before, then repeatedly attempt
the curl -sf http://127.0.0.1:8000/admin/login request (with short
sleeps/backoff between attempts) and exit CI with failure if the endpoint does
not respond within a reasonable timeout; this removes the brittle fixed "sleep
3" and makes the startup check resilient to variable startup times.

178-182: E2Eジョブに timeout-minutes を入れておくと運用が楽ですね

Playwright や外部サービス待ちでハングしたときにランナーを長時間占有しやすいので、ジョブ全体のタイムアウトを付けておくのが無難だと思います。

差分案
   e2e:
     name: Playwright E2E Tests
     needs: lint
     runs-on: ubuntu-latest
+    timeout-minutes: 30

Also applies to: 287-299

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 178 - 182, Add a global job timeout
for the Playwright E2E job by setting the timeout-minutes property on the e2e
job definition (the job named "Playwright E2E Tests" / identifier "e2e") so the
runner is not indefinitely locked by hangs; apply the same change to the other
E2E job block mentioned (the second Playwright E2E job around the other
occurrence) and choose a sensible timeout value (e.g., 30–60 minutes)
appropriate for your test suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 272-275: Replace the fixed sleep before the curl healthcheck with
a retry loop that polls the admin login endpoint until it becomes available or a
timeout is reached: run the PHP built-in server (router.php) in background as
before, then repeatedly attempt the curl -sf http://127.0.0.1:8000/admin/login
request (with short sleeps/backoff between attempts) and exit CI with failure if
the endpoint does not respond within a reasonable timeout; this removes the
brittle fixed "sleep 3" and makes the startup check resilient to variable
startup times.
- Around line 178-182: Add a global job timeout for the Playwright E2E job by
setting the timeout-minutes property on the e2e job definition (the job named
"Playwright E2E Tests" / identifier "e2e") so the runner is not indefinitely
locked by hangs; apply the same change to the other E2E job block mentioned (the
second Playwright E2E job around the other occurrence) and choose a sensible
timeout value (e.g., 30–60 minutes) appropriate for your test suite.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60ed0e89-bf24-4e98-9df5-cd56d3d05627

📥 Commits

Reviewing files that changed from the base of the PR and between a3f1587 and 0a18532.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.github/workflows/lint.yml (1)

9-15: …少し確認したいところがあります。e2e/** パスの除外が必要かもしれません。

test.yml では e2e/** を除外していますが、こちらのワークフローでは含まれたままですね。E2E テストファイル(TypeScript など)の変更時に lint ワークフローが実行される必要があるかどうか、意図通りか確認いただければと思います。

PHP-CS-Fixer と PHPStan は PHP ファイルのみを対象としているので、E2E ディレクトリの変更で実行する必要はないかもしれません。

🔧 必要であれば除外を追加
   paths:
     - '**'
     - '!*.md'
+    - '!e2e/**'
 pull_request:
   paths:
     - '**'
     - '!*.md'
+    - '!e2e/**'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint.yml around lines 9 - 15, lint ワークフロー
(.github/workflows/lint.yml) が e2e/** を除外していないため、E2E テストディレクトリの変更で不要に lint
が走る可能性があります;push と pull_request のそれぞれの paths 配列(現在の paths: - '**' - '!*.md'
ブロック)に '!e2e/**' を追加して test.yml と同様に E2E ファイルを除外するよう更新してください。
.github/workflows/e2e.yml (1)

36-40: …少し気になるところがあります。MailHog サービスにヘルスチェックがないですね。

MySQL サービスにはヘルスチェックが設定されていますが、MailHog にはありません。MailHog の起動が遅れた場合、テストメール送信のテストが不安定になる可能性があるかもしれません。

🔧 ヘルスチェックの追加案
       mailhog:
         image: mailhog/mailhog
         ports:
           - 1025:1025
           - 8025:8025
+        options: >-
+          --health-cmd="wget -q --spider http://localhost:8025/api/v2/messages || exit 1"
+          --health-interval=10s
+          --health-timeout=5s
+          --health-retries=3

ただ、MailHog は比較的軽量なので、実際には問題が起きない可能性も高いです。もし E2E テストでメール関連のフレーキーなテストが発生した場合に検討していただければと思います。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 36 - 40, Add a Docker healthcheck for
the mailhog service to avoid flakey E2E tests when MailHog is slow to start:
update the mailhog service block (service name "mailhog") to include a
healthcheck that probes the HTTP UI on port 8025 (e.g., run a curl -f or similar
CMD test against http://localhost:8025/), and set sensible
interval/timeout/retries (e.g., short interval and a few retries) so the
workflow waits until MailHog is ready before running tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/router.php`:
- Around line 14-22: This router is being loaded outside the PHP built-in web
server causing undefined $_SERVER['REQUEST_URI'] and missing
__DIR__.'/index.php' when run via CLI; add a guard at the top of this router.php
that ensures it only runs under the built-in server (check PHP_SAPI ===
'cli-server') and that $_SERVER['REQUEST_URI'] is set, and if not, return/stop
early; also before require __DIR__.'/index.php' verify that
file_exists(__DIR__.'/index.php') (or avoid requiring when not present) so the
script never attempts to parse URLs or include index.php in non-server contexts
(references: $_SERVER['REQUEST_URI'], parse_url, is_file, preg_match,
__DIR__.'/index.php').

---

Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 36-40: Add a Docker healthcheck for the mailhog service to avoid
flakey E2E tests when MailHog is slow to start: update the mailhog service block
(service name "mailhog") to include a healthcheck that probes the HTTP UI on
port 8025 (e.g., run a curl -f or similar CMD test against
http://localhost:8025/), and set sensible interval/timeout/retries (e.g., short
interval and a few retries) so the workflow waits until MailHog is ready before
running tests.

In @.github/workflows/lint.yml:
- Around line 9-15: lint ワークフロー (.github/workflows/lint.yml) が e2e/**
を除外していないため、E2E テストディレクトリの変更で不要に lint が走る可能性があります;push と pull_request のそれぞれの
paths 配列(現在の paths: - '**' - '!*.md' ブロック)に '!e2e/**' を追加して test.yml と同様に E2E
ファイルを除外するよう更新してください。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84bcf43f-abdb-4945-a326-03c1b65818d4

📥 Commits

Reviewing files that changed from the base of the PR and between 0a18532 and 3e4b796.

📒 Files selected for processing (4)
  • .github/workflows/e2e.yml
  • .github/workflows/lint.yml
  • .github/workflows/test.yml
  • e2e/router.php

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
.github/workflows/e2e.yml (4)

62-64: Composer の --no-suggest フラグについて

--no-suggest フラグは Composer 2.x で非推奨になっているかもしれません。動作には影響しませんが、将来的に削除しても問題ないと思います。

♻️ 修正案
       - name: Install EC-CUBE dependencies
         working-directory: eccube
-        run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader --no-interaction
+        run: composer install --no-progress --prefer-dist --optimize-autoloader --no-interaction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 62 - 64, The workflow step named
"Install EC-CUBE dependencies" uses composer install with the deprecated
--no-suggest flag; update the run command in that step (the line containing
"composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader
--no-interaction") by removing --no-suggest (or conditionally guard it based on
Composer version if you need compatibility) so the command becomes composer
install --no-progress --prefer-dist --optimize-autoloader --no-interaction.

71-88: 環境変数の重複が少し気になりますね

同じ環境変数(DATABASE_URLMAILER_DSNなど)が複数のステップで繰り返し定義されています。ジョブレベルで env を定義すると、メンテナンス性が向上するかもしれません。

♻️ 修正案

ジョブレベルに環境変数を移動する:

 jobs:
   e2e:
     name: Playwright E2E Tests
     runs-on: ubuntu-latest
+    env:
+      APP_ENV: 'dev'
+      APP_DEBUG: '1'
+      DATABASE_URL: 'mysql://root:root@127.0.0.1:3306/eccube_db'
+      DATABASE_SERVER_VERSION: '8.0'
+      DATABASE_CHARSET: 'utf8mb4'
+      MAILER_DSN: 'smtp://127.0.0.1:1025'
+      ECCUBE_AUTH_MAGIC: 'e2etestmagic'

     services:
       mysql:

そして、各ステップから環境変数の定義を削除します。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 71 - 88, The "Setup EC-CUBE" step
repeats several environment variables (APP_ENV, APP_DEBUG, DATABASE_URL,
DATABASE_SERVER_VERSION, DATABASE_CHARSET, MAILER_DSN, ECCUBE_AUTH_MAGIC); move
these env definitions up to the job-level env in .github/workflows/e2e.yml so
they apply to all steps, then remove the env block from the "Setup EC-CUBE" step
(the run block invoking php bin/console commands like doctrine:database:create,
eccube:fixtures:load, eccube:plugin:install/enable/schema-update, cache:warmup
should remain unchanged).

66-69: プラグインのコピー方法について、少し気になるところがあります

現在の find コマンドを使った方法も動作しますが、隠しファイルの扱いなど、少し繊細な部分があるかもしれません。rsync を使うと、より明示的に除外パターンを指定できて読みやすくなると思います。

♻️ 別の実装案
       - name: Install plugin
         run: |
           mkdir -p eccube/app/Plugin/StockAlertMail
-          find plugin -mindepth 1 -maxdepth 1 ! -name '.github' ! -name '.git' -exec cp -r {} eccube/app/Plugin/StockAlertMail/ \;
+          rsync -a --exclude='.github' --exclude='.git' plugin/ eccube/app/Plugin/StockAlertMail/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 66 - 69, The current "Install plugin"
step uses a fragile find-based copy; replace that find invocation with rsync to
explicitly exclude .github and .git and to preserve attributes; specifically, in
the run block that currently creates eccube/app/Plugin/StockAlertMail and runs
the find ... cp command, change it to create the target directory and use rsync
with archive mode and explicit --exclude patterns for '.github' and '.git' (and
any other repo meta directories you want to skip) to copy the contents of
plugin/ into eccube/app/Plugin/StockAlertMail/.

90-114: サーバー起動の待機処理について

sleep 3 で固定時間待機していますが、CI環境の負荷によっては起動が間に合わない可能性があるかもしれません。より確実な方法として、リトライ付きのヘルスチェックループを使うと、テストの安定性が向上すると思います。

♻️ より堅牢な実装案
           cat router.php
           php -l router.php
           php -S 127.0.0.1:8000 router.php &
-          sleep 3
-          curl -v http://127.0.0.1:8000/admin/login 2>&1 | head -30
+          # Wait for server to be ready with retries
+          for i in {1..30}; do
+            if curl -f -s http://127.0.0.1:8000/admin/login > /dev/null 2>&1; then
+              echo "Server is ready!"
+              break
+            fi
+            echo "Waiting for server... ($i/30)"
+            sleep 1
+          done
+          curl -v http://127.0.0.1:8000/admin/login 2>&1 | head -30
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 90 - 114, Replace the fixed sleep 3
with a retrying health-check loop that probes the PHP built-in server started
with "php -S 127.0.0.1:8000 router.php" using curl against the same admin login
URL; implement repeated attempts (e.g., up to a configurable max attempts) with
a short delay between tries, exit the loop successfully when curl returns a
successful HTTP response or non-empty body, and fail the job if the max attempts
are exhausted—update the Start PHP built-in server step to stop using the
literal "sleep 3" and instead use this loop to verify router.php is serving
before proceeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/e2e.yml:
- Around line 9-15: Update the path filters so Markdown exclusions apply
recursively: replace the '!*.md' pattern with '!**/*.md' in both the push and
pull_request `paths` entries to exclude markdown files in subdirectories, and
optionally remove the redundant '**' entries (since the default is to include
all paths) to simplify the workflow.

---

Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 62-64: The workflow step named "Install EC-CUBE dependencies" uses
composer install with the deprecated --no-suggest flag; update the run command
in that step (the line containing "composer install --no-progress --no-suggest
--prefer-dist --optimize-autoloader --no-interaction") by removing --no-suggest
(or conditionally guard it based on Composer version if you need compatibility)
so the command becomes composer install --no-progress --prefer-dist
--optimize-autoloader --no-interaction.
- Around line 71-88: The "Setup EC-CUBE" step repeats several environment
variables (APP_ENV, APP_DEBUG, DATABASE_URL, DATABASE_SERVER_VERSION,
DATABASE_CHARSET, MAILER_DSN, ECCUBE_AUTH_MAGIC); move these env definitions up
to the job-level env in .github/workflows/e2e.yml so they apply to all steps,
then remove the env block from the "Setup EC-CUBE" step (the run block invoking
php bin/console commands like doctrine:database:create, eccube:fixtures:load,
eccube:plugin:install/enable/schema-update, cache:warmup should remain
unchanged).
- Around line 66-69: The current "Install plugin" step uses a fragile find-based
copy; replace that find invocation with rsync to explicitly exclude .github and
.git and to preserve attributes; specifically, in the run block that currently
creates eccube/app/Plugin/StockAlertMail and runs the find ... cp command,
change it to create the target directory and use rsync with archive mode and
explicit --exclude patterns for '.github' and '.git' (and any other repo meta
directories you want to skip) to copy the contents of plugin/ into
eccube/app/Plugin/StockAlertMail/.
- Around line 90-114: Replace the fixed sleep 3 with a retrying health-check
loop that probes the PHP built-in server started with "php -S 127.0.0.1:8000
router.php" using curl against the same admin login URL; implement repeated
attempts (e.g., up to a configurable max attempts) with a short delay between
tries, exit the loop successfully when curl returns a successful HTTP response
or non-empty body, and fail the job if the max attempts are exhausted—update the
Start PHP built-in server step to stop using the literal "sleep 3" and instead
use this loop to verify router.php is serving before proceeding.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb7ff5c4-5ca7-432f-a69b-3718dada39e0

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4b796 and 412183a.

📒 Files selected for processing (1)
  • .github/workflows/e2e.yml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.github/workflows/e2e.yml (1)

66-69: プラグインのコピー方法が少し複雑かもしれません

find コマンドを使った除外パターンは機能しますが、少し読みにくいかもしれません。rsync を使うと、もう少しシンプルに書けるかもしれませんね。

別の実装案
       - name: Install plugin
         run: |
           mkdir -p eccube/app/Plugin/StockAlertMail
-          find plugin -mindepth 1 -maxdepth 1 ! -name '.github' ! -name '.git' -exec cp -r {} eccube/app/Plugin/StockAlertMail/ \;
+          rsync -av --exclude='.github' --exclude='.git' plugin/ eccube/app/Plugin/StockAlertMail/

ただ、現在の実装でも問題なく動作していると思いますので、お好みで判断していただければと思います。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 66 - 69, Replace the complex find+cp
sequence in the "Install plugin" step with a simpler rsync command: create the
target directory if needed and use rsync -a --exclude='.github' --exclude='.git'
plugin/ eccube/app/Plugin/StockAlertMail/ so exclusions are clearer and the copy
is recursive and atomic; update the step commands to run mkdir -p
eccube/app/Plugin/StockAlertMail && rsync -a --exclude='.github'
--exclude='.git' plugin/ eccube/app/Plugin/StockAlertMail/ to implement this
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/e2e.yml:
- Around line 62-64: In the "Install EC-CUBE dependencies" GitHub Actions step
(name: Install EC-CUBE dependencies) remove the deprecated Composer flag
`--no-suggest` from the `composer install --no-progress --no-suggest
--prefer-dist --optimize-autoloader --no-interaction` command so the step runs
without the `--no-suggest` option (i.e., use `composer install --no-progress
--prefer-dist --optimize-autoloader --no-interaction`) to avoid future Composer
3 incompatibility.

---

Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 66-69: Replace the complex find+cp sequence in the "Install
plugin" step with a simpler rsync command: create the target directory if needed
and use rsync -a --exclude='.github' --exclude='.git' plugin/
eccube/app/Plugin/StockAlertMail/ so exclusions are clearer and the copy is
recursive and atomic; update the step commands to run mkdir -p
eccube/app/Plugin/StockAlertMail && rsync -a --exclude='.github'
--exclude='.git' plugin/ eccube/app/Plugin/StockAlertMail/ to implement this
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2395bc7-ca70-427a-81ed-5741336e9bcb

📥 Commits

Reviewing files that changed from the base of the PR and between 412183a and 3f9214e.

📒 Files selected for processing (1)
  • .github/workflows/e2e.yml

@dotani1111 dotani1111 force-pushed the feature/e2e-tests branch 2 times, most recently from cedf715 to dead00b Compare March 24, 2026 07:21
@dotani1111
Copy link
Copy Markdown
Owner Author

@CodeRabbit
再度レビューしてください。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

@dotani1111 かしこまりました…再度レビューをさせていただきますね。少々お待ちください。

✅ Actions performed

Full review triggered.

## E2E テスト
管理画面の主要機能をカバーする25件のPlaywright E2Eテストを追加。

- ナビゲーション(サイドバーメニュー)
- 設定画面(閾値変更、通知先設定、バリデーション)
- テストメール送信(MailHog連携)
- メールテンプレート編集
- コマンド実行(アラート送信、重複防止、在庫回復後の再アラート)
- 送信履歴画面
- 異常系(CSRF、エラーメッセージ)

## CI ワークフロー分割
ci.ymlを3つの独立したワークフローに分離し並列実行可能に。
- lint.yml: PHP-CS-Fixer & PHPStan
- test.yml: PHPUnit(4マトリクス)
- e2e.yml: Playwright E2E(Symfony CLI + MySQL + MailHog)

## その他
- テストメールのダミー商品にIDを設定し商品名表示を修正
- paths-ignoreで再帰的にMarkdownを除外
- Composer --no-suggest オプションを削除(Composer 2.0+非推奨)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dotani1111
Copy link
Copy Markdown
Owner Author

@CodeRabbit
お、レビュー中かな。頼んだー。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Tip

For best results, initiate chat on the files or code changes.

@dotani1111 はい…ちゃんとレビューしていますよ。少し時間がかかるかもしれませんが、丁寧に見ていますので、もう少しお待ちくださいね…

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (15)
Service/StockAlertMailBuilder.php (1)

138-146: …少し気になるところがあります。

setAccessible(true) は PHP 8.1 以降では不要になっていますね。PHP 8.1+ では Reflection 経由で private プロパティに直接アクセスできるようになったので、この行は冗長かもしれません。

ただ、EC-CUBE 4.2 は PHP 8.1 対応なので、後方互換性のために残しておいても害はないと思います…お好みで削除しても良いかもしれませんね。

♻️ 冗長な setAccessible の削除案
 private function setEntityId(object $entity, int $id): void
 {
     $ref = new \ReflectionProperty($entity, 'id');
-    $ref->setAccessible(true);
     $ref->setValue($entity, $id);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Service/StockAlertMailBuilder.php` around lines 138 - 146, The method
setEntityId uses ReflectionProperty to write a private 'id' on an entity and
calls $ref->setAccessible(true), which is redundant on PHP 8.1+; remove the
setAccessible(true) call from setEntityId (keep the ReflectionProperty
instantiation and setValue call) to avoid unnecessary code while preserving
behavior — update the setEntityId method (the ReflectionProperty usage and
setValue on the 'id' property) to omit the setAccessible invocation.
e2e/tests/error-cases.spec.ts (2)

28-36: CSRF テストの実装…良いアプローチですね。

CSRF トークンなしの POST が適切に拒否されることを確認するのは、セキュリティテストとして価値があります。

一点だけ…他のテストケースと異なり、このテストにはテスト ID(例: TC-7.4)が付いていないですね。一貫性のためにIDを付与しても良いかもしれません。

♻️ テストIDの追加案
-  test('CSRF トークンなしのテストメール送信は拒否される', async ({ page }) => {
+  test('TC-7.4: CSRF トークンなしのテストメール送信は拒否される', async ({ page }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/error-cases.spec.ts` around lines 28 - 36, Add a consistent test ID
to the CSRF test by updating the test name string in the test(...) call (the
test function that currently reads 'CSRF トークンなしのテストメール送信は拒否される') to include an
identifier such as "TC-7.4" (e.g., prefix or suffix the existing description);
no logic changes required—only modify the test title within the test(...)
invocation so it matches the other test cases' ID convention.

13-14: …少し気になるところがあります。

.isVisible() は通常例外をスローしないので、.catch(() => false) は不要かもしれません。isVisible() は要素が見つからない場合でも false を返すはずですね。

ただ、防御的なコードとして残しておいても害はないので、お好みで判断していただければと思います。

♻️ 簡略化の案
-    const hasSuccess = await page.locator('.alert-success').isVisible().catch(() => false);
-    const hasError = await page.locator('.alert-danger').isVisible().catch(() => false);
+    const hasSuccess = await page.locator('.alert-success').isVisible();
+    const hasError = await page.locator('.alert-danger').isVisible();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/error-cases.spec.ts` around lines 13 - 14, The two assignments for
hasSuccess and hasError use .isVisible().catch(() => false) unnecessarily;
update the code to call page.locator('.alert-success').isVisible() and
page.locator('.alert-danger').isVisible() directly (remove the .catch handlers)
because Locator.isVisible() already returns false when the element is not found,
so simply use the isVisible() results for hasSuccess and hasError.
e2e/playwright.config.ts (1)

8-14: …少し気になるところがあります。

retries: 0trace: 'on-first-retry' の組み合わせが少し矛盾しているかもしれません。リトライが 0 回だと、トレースが取得される機会がないですね…。

デバッグ目的でトレースを活用したい場合は、retries: 1 に変更するか、trace: 'on' または trace: 'retain-on-failure' に変更することをご検討ください。

♻️ トレース設定の修正案

案1: リトライを有効化してトレースを活用

-  retries: 0,
+  retries: 1,

案2: 失敗時に常にトレースを取得

-    trace: 'on-first-retry',
+    trace: 'retain-on-failure',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/playwright.config.ts` around lines 8 - 14, The Playwright config
currently sets retries: 0 while trace: 'on-first-retry', which never captures
traces; update the config in playwright.config.ts so traces are actually
collected by either enabling retries (set retries to 1) to allow
'on-first-retry' to work, or keep retries: 0 and change trace to 'on' or
'retain-on-failure' to always collect traces on failures; adjust the retries
and/or trace properties accordingly to match your desired debugging behavior.
.github/workflows/lint.yml (1)

9-13: …少し気になるところがあります。

paths-ignore**/*.md は除外されていますが、e2e/** は除外されていないですね。E2E ディレクトリの変更(TypeScript/JavaScript)は PHP の Lint には関係ないので、test.yml と同様に除外しても良いかもしれません。

意図的であれば問題ないですが、CI の実行回数を減らせる可能性があります…ご検討いただければと思います。

♻️ e2e ディレクトリを paths-ignore に追加する案
   pull_request:
     paths-ignore:
       - '**/*.md'
+      - 'e2e/**'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint.yml around lines 9 - 13, The workflow currently
ignores only '**/*.md' under the top-level paths-ignore and the
pull_request.paths-ignore; add 'e2e/**' to both paths-ignore lists so changes
inside the e2e directory won't trigger the PHP lint workflow (update the
top-level paths-ignore and the pull_request -> paths-ignore entries in
.github/workflows/lint.yml to include 'e2e/**' alongside '**/*.md').
e2e/package.json (1)

1-13: …素敵な設定ですね。シンプルで整っていると思います。

必要最小限の構成で、Playwrightの基本的な使い方(headless、headed、UI モード)がカバーされていますし、private: true も適切ですね。^1.50.0 の指定であれば、自動的により新しいバージョンへの更新も対応されるかと思います。

一点、package-lock.json をコミットしておくと、CI環境での再現性がより確実になるかもしれませんね。よろしければご検討ください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/package.json` around lines 1 - 13, Add and commit a package-lock.json
alongside the existing package.json to ensure CI reproducibility: run npm
install (or npm ci) to generate package-lock.json from the current
devDependencies (referencing the "devDependencies" entry and Playwright version
"^1.50.0" in package.json), ensure package-lock.json is not excluded by
.gitignore, and include the generated package-lock.json in the repo so CI
installs deterministic versions.
e2e/tests/log.spec.ts (2)

61-67: afterAll フックの配置について。

test.describe ブロックの外に afterAll が配置されていますね。これは意図的かもしれませんが、describe 内に移動した方がスコープが明確になると思います。

♻️ 修正案
 test.describe('送信履歴画面', () => {
+  test.afterAll(async ({ browser }) => {
+    // 閾値をデフォルトに戻す
+    const context = await browser.newContext({ storageState: './auth.json' });
+    const page = await context.newPage();
+    await setThreshold(page, 5);
+    await context.close();
+  });
+
   test('TC-6.1: 送信履歴画面が正常に表示される', async ({ page }) => {
     // ...
   });
   // ... other tests
 });
-
-test.afterAll(async ({ browser }) => {
-  // 閾値をデフォルトに戻す
-  const context = await browser.newContext({ storageState: './auth.json' });
-  const page = await context.newPage();
-  await setThreshold(page, 5);
-  await context.close();
-});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/log.spec.ts` around lines 61 - 67, The teardown hook test.afterAll
is declared outside the related test.describe which makes its scope ambiguous;
move the entire test.afterAll block (the code that creates a newContext with
storageState './auth.json', opens a newPage, calls setThreshold(page, 5), and
closes the context) into the corresponding test.describe block so it runs only
for that suite and clearly scopes cleanup to those tests.

52-57: 条件付きスキップのパターンについて。

test.skip() を条件分岐内で使うのは有効ですが、テストレポートでスキップ理由が分かりにくくなることがありますね。メッセージを追加すると、後で確認しやすいかもしれません。

♻️ 修正案
     if (isVisible) {
       await productLink.click();
       await expect(page).toHaveURL(/\/admin\/product\/product\/\d+\/edit/);
     } else {
-      test.skip();
+      test.skip(true, '商品リンクが表示されていないためスキップ');
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/log.spec.ts` around lines 52 - 57, The conditional skip uses
test.skip() without a reason which makes reporting unclear; update the branch
that calls test.skip() to pass a descriptive message (e.g., test.skip('product
link not visible: skipping navigation/assertion')) so the test report explains
why it was skipped; locate the isVisible check and the block around
productLink.click() / await expect(page).toHaveURL(...) and replace the bare
test.skip() with a message-reflecting skip.
e2e/tests/command.spec.ts (1)

97-103: afterAll の配置について。

log.spec.ts でも同様のコメントをしましたが、afterAlldescribe ブロックの外に配置されていますね。一貫性を保つため、どちらかのパターンに統一すると良いかもしれません。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/command.spec.ts` around lines 97 - 103, The test teardown using
test.afterAll is currently defined outside the describe block; to keep placement
consistent with log.spec.ts, move the test.afterAll teardown (the block that
creates a new context, sets threshold via setThreshold(page, 5), and closes the
context) so it sits inside the same describe block that contains the related
tests, or alternatively move the afterAll from log.spec.ts outside to match this
file—pick one pattern and apply it to both files; adjust any references to
page/context accordingly so the teardown still can access the required fixtures.
e2e/tests/test-mail.spec.ts (1)

43-51: メール未送信の確認パターンについて、少し気になるところがあります。

try/catch でメールが届かないことを確認するパターンは機能しますが、expect(true).toBe(false) は失敗メッセージが分かりにくいですね。より明示的な方法もあるかもしれません。

♻️ 代替案
     // 少し待ってもメールが届かないことを確認
     await page.waitForTimeout(2000);
-    try {
-      await waitForMessage(() => true, 1000);
-      // ここに到達したらメールが届いてしまっている
-      expect(true).toBe(false);
-    } catch {
-      // メールが届かない = 期待通り
-    }
+    const messages = await getMessages();
+    expect(messages.length).toBe(0);

getMessages() を import に追加する必要がありますね:

 import {
   deleteAllMessages,
   waitForMessage,
+  getMessages,
   getSubject,
   getBody,
   getTo,
 } from '../helpers/mailhog';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/test-mail.spec.ts` around lines 43 - 51, Replace the try/catch
pattern that asserts failure via expect(true).toBe(false) with an explicit
assertion that no emails exist: call getMessages() (ensure you add getMessages
to the imports) after the wait timeout and assert its result is empty (e.g.,
expect(await getMessages()).toHaveLength(0) or expect(messages).toEqual([]));
keep the existing waitForTimeout(2000) and remove the
try/catch/expect(true).toBe(false) block, referencing waitForMessage only to
understand the original intent.
e2e/tests/mail-template.spec.ts (1)

57-59: 固定タイムアウトの使用について、少し気になります。

waitForTimeout(1000) は CI 環境によって不安定になることがありますね。代わりに、ページの状態を確認する方法も検討できるかもしれません。

ただ、このケースではバリデーションエラーの確認なので、現状でも許容範囲だと思います。

♻️ 代替案
     // 送信ボタンをクリック(HTML5 required バリデーションで送信されない場合もある)
     await page.locator('button.btn-ec-conversion[type="submit"]').click();
 
-    // 成功メッセージが表示されないことを確認
-    await page.waitForTimeout(1000);
-    await expect(page.locator('.alert-success')).not.toBeVisible();
+    // バリデーションエラーまたはページが変わらないことを確認
+    await page.waitForLoadState('networkidle');
+    await expect(page.locator('.alert-success')).not.toBeVisible({ timeout: 2000 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/mail-template.spec.ts` around lines 57 - 59, Replace the fixed
sleep (page.waitForTimeout(1000)) with an explicit wait for the element state so
the check is stable in CI: remove the page.waitForTimeout call and instead wait
for the '.alert-success' locator to be hidden/absent using Playwright's built-in
waiting (e.g., locator.waitFor or page.waitForSelector with state:'hidden', or
use expect(page.locator('.alert-success')).not.toBeVisible({ timeout: ... }));
update the test around the '.alert-success' locator and the existing expect to
rely on that deterministic wait rather than a fixed timeout.
e2e/tests/config.spec.ts (1)

64-68: 親要素へのトラバースについて、少し気になるところがあります。

locator('..') を使った親要素への移動は、DOM構造の変更に対して脆弱になることがありますね。もし可能であれば、よりセマンティックなセレクタを使うと保守しやすいかもしれません。

♻️ 代替案
-    const helpText = page.locator('#stock_alert_config_alertEmails')
-      .locator('..').locator('.form-text');
+    // フォームグループ全体から help text を探す
+    const helpText = page.locator('.form-text:near(`#stock_alert_config_alertEmails`)');
+    // または、より明示的に
+    // const helpText = page.locator('#stock_alert_config_alertEmails + .form-text, `#stock_alert_config_alertEmails` ~ .form-text');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/tests/config.spec.ts` around lines 64 - 68, The test uses fragile parent
traversal via locator('..') on helpText; replace this with a direct, semantic
selector targeting the help text for the field (e.g. a descendant selector
combining '#stock_alert_config_alertEmails' with '.form-text' or use a
role/test-id-based locator) and update the helpText reference accordingly so the
assertion await expect(helpText).toContainText('空欄の場合は店舗設定のメールアドレスを使用します') uses
the new stable locator instead of locator('..').
.github/workflows/e2e.yml (1)

141-157: actions/upload-artifact@v6 は v7.0.0 が最新版のようですね。

2026年2月26日にリリースされたばかりです。v6 でも動作しますが、最新版への更新を検討してみても良いかもしれません。特に新機能で単一ファイルを直接アップロードできるようになっているので、将来的には便利かもしれませんね。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/e2e.yml around lines 141 - 157, Update the two GitHub
Actions steps named "Upload Playwright report" and "Upload test results" to use
the latest upload-artifact action by replacing uses: actions/upload-artifact@v6
with uses: actions/upload-artifact@v7; verify the steps (Upload Playwright
report and Upload test results) still accept the same inputs (name, path,
retention-days, if-no-files-found) or adjust input names to match v7 if any
changed.
e2e/helpers/mailhog.ts (2)

66-78: …ちょっとだけ気になる点があります

charset を _charset として捨てていますね(Line 70)。テスト対象のシステムがUTF-8のみを使うなら問題ないと思いますが、もし将来 ISO-2022-JP など他のエンコーディングが来た場合、文字化けするかもしれませんね。

今のままでもテストとしては十分機能すると思いますので、余裕があれば考慮いただければ…という程度ですね。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/mailhog.ts` around lines 66 - 78, The function decodeMimeEncoded
currently ignores the captured charset (named _charset) which will break
non-UTF-8 encoded words; update decodeMimeEncoded to use the captured charset
(e.g., rename _charset to charset) and decode using that charset in both 'B'
(base64) and 'Q' (quoted-printable) branches—use Buffer.from(..., 'base64')
followed by a charset-aware decode (via iconv-lite.decode or equivalent) for
'B', and after converting underscores/quoted-printable for 'Q' run a
charset-aware decode (iconv-lite.decode) instead of assuming 'utf-8', falling
back to 'utf-8' if the charset is unsupported. Ensure references to
decodeQuotedPrintableUtf8 are adapted or replaced so it accepts the charset or
returns raw bytes for charset-aware decoding.

22-32: …少し気になるところがあります。fetch のエラーハンドリングが省略されていますね

deleteAllMessages()getMessages() で fetch のレスポンスステータスを確認していないですね。MailHog が起動していない場合や、APIがエラーを返した場合に、テストが不明瞭なエラーで失敗するかもしれません。

テストヘルパーなので致命的ではないと思いますが、デバッグしやすくするために、レスポンスチェックを追加しておくと親切かもしれませんね。

🛠️ 提案する修正
 /** 全メールを削除する */
 export async function deleteAllMessages(): Promise<void> {
-  await fetch(`${MAILHOG_API}/v1/messages`, { method: 'DELETE' });
+  const res = await fetch(`${MAILHOG_API}/v1/messages`, { method: 'DELETE' });
+  if (!res.ok) {
+    throw new Error(`MailHog API error: ${res.status} ${res.statusText}`);
+  }
 }

 /** 全メールを取得する(新しい順) */
 export async function getMessages(): Promise<MailhogMessage[]> {
   const res = await fetch(`${MAILHOG_API}/v2/messages`);
+  if (!res.ok) {
+    throw new Error(`MailHog API error: ${res.status} ${res.statusText}`);
+  }
   const data: MailhogResponse = await res.json();
   return data.items;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/helpers/mailhog.ts` around lines 22 - 32, Both deleteAllMessages and
getMessages lack fetch response checks; update deleteAllMessages to inspect the
response (res.ok) and throw or log a descriptive error including status and
response text when the DELETE fails, and update getMessages to check res.ok
before calling res.json(), throwing a detailed error with status and body on
failure; reference the functions deleteAllMessages and getMessages and include
the response status and body text in the error to aid debugging when MailHog is
down or API errors occur.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/helpers/command.ts`:
- Around line 4-5: The fallback path for ECCUBE_ROOT in the ECCUBE_ROOT constant
is too shallow (../../../../..) and lands in app/ instead of reaching the
project root eccube/ when the plugin is installed under
eccube/app/Plugin/StockAlertMail/; update the fallback to climb one more level
(use ../../../../../.. instead) so ECCUBE_ROOT resolves correctly from
e2e/helpers/, i.e. change the default path expression used to compute
ECCUBE_ROOT in this file.

In `@e2e/helpers/pages.ts`:
- Around line 35-36: The dialog listener registered with page.on('dialog', ...)
is permanent and can accumulate across multiple calls (e.g., from sendTestMail);
change it to a one-time handler by using page.once('dialog', dialog =>
dialog.accept()) or, if you must keep page.on, store the callback and remove it
after use with page.off('dialog', handler). Update the code around the dialog
registration in sendTestMail (or the helper where page.on is called) to use
page.once or to call page.off after accepting so listeners don't leak.

In `@e2e/tests/log.spec.ts`:
- Around line 22-39: The test times out because table.isVisible() may run before
the page finishes loading; update the check to wait for the table or its
container first (use page.waitForSelector or expect(table).toBeVisible with a
timeout) before calling table.isVisible(), and use the same waiting approach
before asserting productLink visibility (table.locator(...).first()) and before
the empty-state check on page.locator('.card-body') so assertions only run after
the DOM element is present.

---

Nitpick comments:
In @.github/workflows/e2e.yml:
- Around line 141-157: Update the two GitHub Actions steps named "Upload
Playwright report" and "Upload test results" to use the latest upload-artifact
action by replacing uses: actions/upload-artifact@v6 with uses:
actions/upload-artifact@v7; verify the steps (Upload Playwright report and
Upload test results) still accept the same inputs (name, path, retention-days,
if-no-files-found) or adjust input names to match v7 if any changed.

In @.github/workflows/lint.yml:
- Around line 9-13: The workflow currently ignores only '**/*.md' under the
top-level paths-ignore and the pull_request.paths-ignore; add 'e2e/**' to both
paths-ignore lists so changes inside the e2e directory won't trigger the PHP
lint workflow (update the top-level paths-ignore and the pull_request ->
paths-ignore entries in .github/workflows/lint.yml to include 'e2e/**' alongside
'**/*.md').

In `@e2e/helpers/mailhog.ts`:
- Around line 66-78: The function decodeMimeEncoded currently ignores the
captured charset (named _charset) which will break non-UTF-8 encoded words;
update decodeMimeEncoded to use the captured charset (e.g., rename _charset to
charset) and decode using that charset in both 'B' (base64) and 'Q'
(quoted-printable) branches—use Buffer.from(..., 'base64') followed by a
charset-aware decode (via iconv-lite.decode or equivalent) for 'B', and after
converting underscores/quoted-printable for 'Q' run a charset-aware decode
(iconv-lite.decode) instead of assuming 'utf-8', falling back to 'utf-8' if the
charset is unsupported. Ensure references to decodeQuotedPrintableUtf8 are
adapted or replaced so it accepts the charset or returns raw bytes for
charset-aware decoding.
- Around line 22-32: Both deleteAllMessages and getMessages lack fetch response
checks; update deleteAllMessages to inspect the response (res.ok) and throw or
log a descriptive error including status and response text when the DELETE
fails, and update getMessages to check res.ok before calling res.json(),
throwing a detailed error with status and body on failure; reference the
functions deleteAllMessages and getMessages and include the response status and
body text in the error to aid debugging when MailHog is down or API errors
occur.

In `@e2e/package.json`:
- Around line 1-13: Add and commit a package-lock.json alongside the existing
package.json to ensure CI reproducibility: run npm install (or npm ci) to
generate package-lock.json from the current devDependencies (referencing the
"devDependencies" entry and Playwright version "^1.50.0" in package.json),
ensure package-lock.json is not excluded by .gitignore, and include the
generated package-lock.json in the repo so CI installs deterministic versions.

In `@e2e/playwright.config.ts`:
- Around line 8-14: The Playwright config currently sets retries: 0 while trace:
'on-first-retry', which never captures traces; update the config in
playwright.config.ts so traces are actually collected by either enabling retries
(set retries to 1) to allow 'on-first-retry' to work, or keep retries: 0 and
change trace to 'on' or 'retain-on-failure' to always collect traces on
failures; adjust the retries and/or trace properties accordingly to match your
desired debugging behavior.

In `@e2e/tests/command.spec.ts`:
- Around line 97-103: The test teardown using test.afterAll is currently defined
outside the describe block; to keep placement consistent with log.spec.ts, move
the test.afterAll teardown (the block that creates a new context, sets threshold
via setThreshold(page, 5), and closes the context) so it sits inside the same
describe block that contains the related tests, or alternatively move the
afterAll from log.spec.ts outside to match this file—pick one pattern and apply
it to both files; adjust any references to page/context accordingly so the
teardown still can access the required fixtures.

In `@e2e/tests/config.spec.ts`:
- Around line 64-68: The test uses fragile parent traversal via locator('..') on
helpText; replace this with a direct, semantic selector targeting the help text
for the field (e.g. a descendant selector combining
'#stock_alert_config_alertEmails' with '.form-text' or use a role/test-id-based
locator) and update the helpText reference accordingly so the assertion await
expect(helpText).toContainText('空欄の場合は店舗設定のメールアドレスを使用します') uses the new stable
locator instead of locator('..').

In `@e2e/tests/error-cases.spec.ts`:
- Around line 28-36: Add a consistent test ID to the CSRF test by updating the
test name string in the test(...) call (the test function that currently reads
'CSRF トークンなしのテストメール送信は拒否される') to include an identifier such as "TC-7.4" (e.g.,
prefix or suffix the existing description); no logic changes required—only
modify the test title within the test(...) invocation so it matches the other
test cases' ID convention.
- Around line 13-14: The two assignments for hasSuccess and hasError use
.isVisible().catch(() => false) unnecessarily; update the code to call
page.locator('.alert-success').isVisible() and
page.locator('.alert-danger').isVisible() directly (remove the .catch handlers)
because Locator.isVisible() already returns false when the element is not found,
so simply use the isVisible() results for hasSuccess and hasError.

In `@e2e/tests/log.spec.ts`:
- Around line 61-67: The teardown hook test.afterAll is declared outside the
related test.describe which makes its scope ambiguous; move the entire
test.afterAll block (the code that creates a newContext with storageState
'./auth.json', opens a newPage, calls setThreshold(page, 5), and closes the
context) into the corresponding test.describe block so it runs only for that
suite and clearly scopes cleanup to those tests.
- Around line 52-57: The conditional skip uses test.skip() without a reason
which makes reporting unclear; update the branch that calls test.skip() to pass
a descriptive message (e.g., test.skip('product link not visible: skipping
navigation/assertion')) so the test report explains why it was skipped; locate
the isVisible check and the block around productLink.click() / await
expect(page).toHaveURL(...) and replace the bare test.skip() with a
message-reflecting skip.

In `@e2e/tests/mail-template.spec.ts`:
- Around line 57-59: Replace the fixed sleep (page.waitForTimeout(1000)) with an
explicit wait for the element state so the check is stable in CI: remove the
page.waitForTimeout call and instead wait for the '.alert-success' locator to be
hidden/absent using Playwright's built-in waiting (e.g., locator.waitFor or
page.waitForSelector with state:'hidden', or use
expect(page.locator('.alert-success')).not.toBeVisible({ timeout: ... }));
update the test around the '.alert-success' locator and the existing expect to
rely on that deterministic wait rather than a fixed timeout.

In `@e2e/tests/test-mail.spec.ts`:
- Around line 43-51: Replace the try/catch pattern that asserts failure via
expect(true).toBe(false) with an explicit assertion that no emails exist: call
getMessages() (ensure you add getMessages to the imports) after the wait timeout
and assert its result is empty (e.g., expect(await
getMessages()).toHaveLength(0) or expect(messages).toEqual([])); keep the
existing waitForTimeout(2000) and remove the try/catch/expect(true).toBe(false)
block, referencing waitForMessage only to understand the original intent.

In `@Service/StockAlertMailBuilder.php`:
- Around line 138-146: The method setEntityId uses ReflectionProperty to write a
private 'id' on an entity and calls $ref->setAccessible(true), which is
redundant on PHP 8.1+; remove the setAccessible(true) call from setEntityId
(keep the ReflectionProperty instantiation and setValue call) to avoid
unnecessary code while preserving behavior — update the setEntityId method (the
ReflectionProperty usage and setValue on the 'id' property) to omit the
setAccessible invocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86b3de0c-1c26-455b-b379-4afeabd15881

📥 Commits

Reviewing files that changed from the base of the PR and between 12e3e28 and dead00b.

⛔ Files ignored due to path filters (1)
  • e2e/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • .github/workflows/e2e.yml
  • .github/workflows/lint.yml
  • .github/workflows/test.yml
  • Service/StockAlertMailBuilder.php
  • e2e/.gitignore
  • e2e/helpers/command.ts
  • e2e/helpers/mailhog.ts
  • e2e/helpers/pages.ts
  • e2e/package.json
  • e2e/playwright.config.ts
  • e2e/tests/command.spec.ts
  • e2e/tests/config.spec.ts
  • e2e/tests/error-cases.spec.ts
  • e2e/tests/global-setup.ts
  • e2e/tests/log.spec.ts
  • e2e/tests/mail-template.spec.ts
  • e2e/tests/navigation.spec.ts
  • e2e/tests/test-mail.spec.ts

- ECCUBE_ROOTフォールバックパスの階層数を修正(5→6階層)
- dialog handlerをpage.on→page.onceに変更(リスナー蓄積防止)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dotani1111 dotani1111 merged commit 9cee51c into main Mar 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant